package breadbox.pastry.routing;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import breadbox.pastry.Conf;
import breadbox.pastry.PastryNode;

/**
 * Keeps track of nodes that this node routes to
 * @author Daniel
 *
 */
public class RoutingTable implements Serializable {
	private static final long serialVersionUID = 1379512986948125547L;
	private List< RoutingNode[ ] > table_;
	private RoutingNode base_node_;
	private ReadWriteLock lock_;
	
	// each row has 2^B columns
	private static final int k_cols_ = (int) Math.ceil( Math.pow( 2.0, Conf.b( ) ) );
	private static final int k_base_ = (int) Math.pow( 2, Conf.b( ) );
	
	public RoutingTable( RoutingTable other ) {
		lock_ = new ReentrantReadWriteLock();
		table_ = new ArrayList< RoutingNode[ ] >( );
		
		if( other != null ) {
			base_node_ = other.baseNode();
			int rows = other.rows();
			for( int i = 0; i < rows; i++ ) {
				RoutingNode[ ] row = other.getRow( i );
				
				if( row != null ) {
					for( RoutingNode node : row ) {
						insert( node );
					}
				}
			}
		}
	}
	
	public RoutingTable( /*RoutingNode base*/ )	{
//		base_node_ = base;
		table_ = new ArrayList< RoutingNode[ ] >( );
		lock_ = new ReentrantReadWriteLock( );
	}
	
	public int base( ) {
		return k_base_;
	}
	
	private RoutingNode baseNode( ) {
		lock_.readLock().lock();
		RoutingNode ret = new RoutingNode( base_node_ );
		lock_.readLock().unlock();
		return ret;
	}
	
	public void setBaseNode( RoutingNode base ) {
		lock_.writeLock( ).lock( );
		base_node_ = base;
		base_node_.setType( RoutingNode.RoutingNodeType.ROUTE );
		lock_.writeLock( ).unlock( );
	}
	
	public RoutingNode get( int row, int col ) {
		RoutingNode ret = null;
		lock_.readLock( ).lock( );
		
		if( row < table_.size( ) && col < k_cols_ )	{
			if( table_.get( row )[ col ] != null )
				ret = new RoutingNode( table_.get( row )[ col ] );
		} 
		
		lock_.readLock( ).unlock( );
		return ret;
	}
	
	public RoutingNode[ ] getRow( int row ) {
		if( row >= 0 && row < table_.size() ) {
			lock_.readLock().lock();
			RoutingNode[ ] nodes = table_.get( row );
			RoutingNode[ ] ret = null;
			
			if( nodes != null ) {
				ret = new RoutingNode[ nodes.length ];
				
				for( int i = 0; i < nodes.length; i++ ) {
					ret[ i ] = nodes[ i ] == null ? null : new RoutingNode( nodes[ i ] );
				}
			}
			lock_.readLock().unlock();
			return ret;
		}
		
		return null;
	}
	
	public int rows( ) {
		lock_.readLock().lock();
		
		int size = table_.size();
		
		lock_.readLock().unlock();
		
		return size;
	}
	
	public int cols( ) {
		lock_.readLock().lock();
		int cols = k_cols_;
		lock_.readLock().unlock();
		return cols;
	}
	
	public void insert( RoutingNode rn ) {
		if( rn == null )
			return;
		
		lock_.writeLock().lock();
		String cp = base_node_.getId( ).commonPrefix( rn.getId( ) );
		rn.setDistance(PastryNode.getPastryNode().proximity(rn));
		rn.setType( RoutingNode.RoutingNodeType.ROUTE );
		
		// the row to insert rn at
		int row = cp.length( );
		int col = -1;
		
		if( row + 1 < rn.getId( ).toString( k_base_ ).length( ) ) {
			try	{
				// the digit in the
				// row + 1 position when rn.id is expressed in base 2^b.
				String converted_id = rn.getId( ).toString( k_base_ );
				col = Integer.parseInt( converted_id.substring( row + 1, row + 2 ), k_base_ );
			} catch( NumberFormatException e ) {
				lock_.writeLock().unlock();
				return;
			}
			
			// if not enough rows to accommodate the new node,
			// make more rows.
			if( table_.size( ) <= row ) {
				for( int i = table_.size( ) - 1; i <= row; i++ ) {
					table_.add( new RoutingNode[ k_cols_ ] );
				}
			}
			
			RoutingNode old = table_.get( row )[ col ];
			if( old == null || (old != null && old.getDistance() > rn.getDistance() ) )
				table_.get( row )[ col ] = rn; 
		}
		lock_.writeLock().unlock();
	}
	
	public RoutingNode remove( int row, int col ) {
		RoutingNode removed = null;
		
		lock_.writeLock().lock();
		if( row < table_.size( ) && col < k_cols_ ) {
			removed = table_.get( row )[ col ];
			table_.get( row )[ col ] = null;
		}
		
		lock_.writeLock().unlock();
		
		return removed;
	}
	
	public RoutingNode remove( RoutingNode target ) {
		if( target == null )
			return null;
		
		lock_.readLock().lock();
		String cp = base_node_.getId( ).commonPrefix( target.getId( ) );
		
		// the row to remove target from
		int row = cp.length( );
		int col = -1;
		
		if( row + 1 < target.getId( ).toString( k_base_ ).length( ) ) {
			try	{
				// the digit in the
				// row + 1 position when rn.id is expressed in base 2^b.
				String converted_id = target.getId( ).toString( k_base_ );
				col = Integer.parseInt( converted_id.substring( row + 1, row + 2 ), k_base_ );
			} catch( NumberFormatException e ) {
				lock_.readLock().unlock();
				return null;
			}
			
			lock_.readLock().unlock();
			return remove(row, col);
		}

		lock_.readLock().unlock();
		return null;
	}
}
